package com.ww.microrpc.rpc;

import com.ww.microrpc.rpc.InvocationHandlerFactory.MethodHandler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class ReflectiveMicroRpc extends MicroRpc {

    private final ParseHandlersByName targetToHandlersByName;
    private final InvocationHandlerFactory factory;

    public ReflectiveMicroRpc(ParseHandlersByName targetToHandlersByName,
                              InvocationHandlerFactory factory) {
        this.targetToHandlersByName = targetToHandlersByName;
        this.factory = factory;
    }

    static final class ParseHandlersByName {

        private final Contract contract;
        //        private final Options options;
//        private final Encoder encoder;
//        private final Decoder decoder;
//        private final ErrorDecoder errorDecoder;
//        private final QueryMapEncoder queryMapEncoder;
        private final SynchronousMethodHandler.Factory factory;

//        ParseHandlersByName(
//                Contract contract,
//                Options options,
//                Encoder encoder,
//                Decoder decoder,
//                QueryMapEncoder queryMapEncoder,
//                ErrorDecoder errorDecoder,
//                SynchronousMethodHandler.Factory factory) {
//            this.contract = contract;
//            this.options = options;
//            this.factory = factory;
//            this.errorDecoder = errorDecoder;
//            this.queryMapEncoder = queryMapEncoder;
//            this.encoder = checkNotNull(encoder, "encoder");
//            this.decoder = checkNotNull(decoder, "decoder");
//        }

        ParseHandlersByName(Contract contract,
                            SynchronousMethodHandler.Factory factory) {
            this.contract = contract;
            this.factory = factory;
        }

        public Map<String, MethodHandler> apply(Target target) {
            List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
            Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
            for (MethodMetadata md : metadata) {
//                BuildTemplateByResolvingArgs buildTemplate;
//                if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
//                    buildTemplate =
//                            new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
//                } else if (md.bodyIndex() != null) {
//                    buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
//                } else {
//                    buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
//                }
                if (md.isIgnored()) {
                    result.put(md.configKey(), args -> {
                        throw new IllegalStateException(md.configKey() + " is not a method handled by MicroRpc");
                    });
                } else {
                    result.put(md.configKey(),
                            factory.create(target, md));
                }
            }
            return result;
        }
    }

    static class MicroRpcInvocationHandler implements InvocationHandler {

        private final Target target;
        private final Map<Method, MethodHandler> dispatch;

        MicroRpcInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
//            this.target = checkNotNull(target, "target");
//            this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
            this.target = target;
            this.dispatch = dispatch;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("equals".equals(method.getName())) {
                try {
                    Object otherHandler =
                            args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    return equals(otherHandler);
                } catch (IllegalArgumentException e) {
                    return false;
                }
            } else if ("hashCode".equals(method.getName())) {
                return hashCode();
            } else if ("toString".equals(method.getName())) {
                return toString();
            }

            return dispatch.get(method).invoke(args);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof MicroRpcInvocationHandler) {
                MicroRpcInvocationHandler other = (MicroRpcInvocationHandler) obj;
                return target.equals(other.target);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return target.hashCode();
        }

        @Override
        public String toString() {
            return target.toString();
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T newInstance(Target<T> target) {
//        Class type = target.getT
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

        /**
         *
         */
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();

        for (Method method : target.type().getMethods()) {
            if (method.getDeclaringClass() == Object.class) {
                continue;
            } else if (Util.isDefault(method)) {
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                methodToHandler.put(method, nameToHandler.get(configKey(target.type(), method)));
            }
        }

        InvocationHandler handler = factory.create(target, methodToHandler);

        return (T) Proxy.newProxyInstance(target.type().getClassLoader(),
                new Class<?>[]{target.type()}, handler);
    }
}
